﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MoneyWentWhere.DataModel.Collections;
using System.IO;

namespace MoneyWentWhere.DataModel.Reports
{
    public abstract class ExpenseReport<ColT>
    {
        readonly TimeSpanUnit _periodUnit;

        // Columns
        protected List<ColT> _columns;
        
        // Rows 
        List<DateTime> _periodStarts = new List<DateTime>();
        Dictionary<DateTime, int> _periodIndexes = new Dictionary<DateTime, int>();

        // Cells
        ITable<decimal> _expenses = new SparseTable<decimal>();

        // Final table
        ITable<String> _table = new SparseTable<string>();

        public ExpenseReport(TimeSpanUnit periodUnit)
        {
            _periodUnit = periodUnit;
        }

        // Creates the report from the data
        public void Create(MoneyDataSet dataSet)
        {
            if (dataSet.Accounts.Count == 0) { return; }

            _columns = CreateColumns(dataSet);

            CreateRows(dataSet.Accounts);
            CalculateCellValues(dataSet.Accounts);

            // Save the table
            _table = CreateTable(_columns, _periodStarts, _expenses);
        }

        public void SaveTable(String filename)
        {
            File.WriteAllText(filename, _table.ToCsvString());
        }

        protected abstract List<ColT> CreateColumns(MoneyDataSet dataSet);

        ITable<String> CreateTable(
            List<ColT> columns, 
            List<DateTime> periods, 
            ITable<decimal> expenses)
        {
            ITable<string> table = new SparseTable<string>();

            // Column headers
            for (int col = 1; col < columns.Count + 1; col++)
            {
                table[0, col] = columns[col - 1].ToString();
            }

            // Rows
            for (int row = 1; row < periods.Count + 1; row++)
            {
                // Row headers
                // TODO: better formatting
                table[row, 0] = periods[row - 1].ToString();

                // Cells
                for (int col = 1; col < columns.Count + 1; col++)
                {
                    decimal amount = expenses[row - 1, col - 1];
                    table[row, col] = amount.ToString();
                }

            }

            return table;
        }

        void CreateRows(IList<Account> accounts)
        {
            // Find out start and end date
            DateTime startDate = DateTime.MaxValue;
            DateTime endDate = DateTime.MinValue;

            foreach (Account ac in accounts)
            {
                if (ac.Transactions.Count == 0) { continue; }
                if (ac.Transactions[0].Date < startDate) 
                { 
                    startDate = ac.Transactions[0].Date; 
                }
                if (ac.Transactions[ac.Transactions.Count - 1].Date > endDate)
                {
                    endDate = ac.Transactions[ac.Transactions.Count - 1].Date;
                }
            }

            // Add period starts until done
            DateTime date = startDate.PeriodStart(_periodUnit);

            while (date <= endDate)
            {
                _periodStarts.Add(date);
                _periodIndexes.Add(date, _periodStarts.Count - 1);

                date = date.Add(date.PeriodDuration(_periodUnit));
            }
        }

        void CalculateCellValues(IList<Account> accounts)
        {
            foreach (Account ac in accounts)
            {
                if (ac.Transactions.Count == 0) { continue; }
                DateTime lastRecoDate = ac.Transactions[0].Date;

                foreach (Transaction tx in ac.Transactions)
                {
                    if (tx.Kind == TransactionKind.Normal &&
                        tx.Amount > 0)
                    {
                        // Add normal expense
                        //_expenses[GetRowIndex(tx.Date),
                        //    GetColumnIndex(tx)] += tx.Amount;
                    }
                    else if (tx.Kind == TransactionKind.Reconcile)
                    {
                        // Spread the reconcile expense
                        SpreadReconcileTx(tx, lastRecoDate);
                        lastRecoDate = tx.Date;
                    }
                }
            }
        }

        void SpreadReconcileTx(Transaction recoTx, DateTime lastRecoDate)
        {
            if (recoTx.Kind != TransactionKind.Reconcile) { throw new ArgumentException("recoTx not be a Reconcile: " + recoTx); }

            TimeSpan recoDuration = recoTx.Date - lastRecoDate;

            // Special case -- reco right after the other
            if (recoDuration == TimeSpan.Zero)
            {
                _expenses[GetRowIndex(recoTx.Date),
                    GetColumnIndex(recoTx)] += recoTx.Amount;
                return;
            }

            // Normal case -- for each slice (period), spread in proportion of
            // slice duration / total duration

            DateTime sliceStart = lastRecoDate;
            while (sliceStart < recoTx.Date)
            {
                DateTime nextSliceStart = sliceStart.PeriodStart(_periodUnit)
                    .Add(sliceStart.PeriodDuration(_periodUnit));

                if (nextSliceStart > recoTx.Date) { nextSliceStart = recoTx.Date; }

                TimeSpan sliceDuration = nextSliceStart - sliceStart;

                double sliceProportion = (double)sliceDuration.Ticks / recoDuration.Ticks;

                decimal sliceValue = recoTx.Amount * (decimal)sliceProportion;

                int row = GetRowIndex(sliceStart.PeriodStart(_periodUnit));
                int col = GetColumnIndex(recoTx);
                _expenses[row, col] += sliceValue;

                //_expenses[row, col] = recoTx.Amount; // DEBUG-ONLY


                sliceStart = nextSliceStart;
            }
        }

        protected abstract int GetColumnIndex(Transaction tx);
        
        protected int GetRowIndex(DateTime date)
        {
            return _periodIndexes[date.PeriodStart(_periodUnit)];
        }

    }
}
